<?php
/**
 * Created by PhpStorm.
 * User: allen_ios
 * Date: 17/1/3
 * Time: 下午5:40
 */

namespace Task\Core;

class CreateGlobalPk
{
    private $mysqlObj;
    private $threshold; //数据库生成主键的最大尝试次数,做锁用
    private $seqTable;  //存放全局主键的数据表

    public function __construct()
    {
        $this->mysqlObj = Database::getInstance('mysql');
        $this->threshold = 100;

        $this->seqTable = "gc_sequence";
    }

    /**
     * 获得全局主键值
     * @param string $seqName  分表对应逻辑表名称,不带分表名称的后缀
     */
    public function getPk($seqName)
    {
        try{
            return self::_getPKbyRedis($seqName);
        } catch (\Exception $ex) {
            //wLog();
            return self::_getPKByDB($seqName);
        }
    }

    /**
     * 通过数据库获取全局主键值,利用数据库锁
     * @param string $seqName 分表对应逻辑表名称,不带分表名称的后缀
     * @return mixed
     */
    private function _getPKByDB($seqName)
    {
        $sql = "SELECT count(*) as cnt FROM gc_sequence where seq_name='{$seqName}'";
        $data = $this->mysqlObj->query($sql)->fetch(\PDO::FETCH_ASSOC);
        $cnt = $data['cnt'];
        if (!$cnt) {
            return false;
        }

        //如果未锁定的,就更新最大主键值,否则循环等待,等循环100次就退出
        for($i=0; $i < $this->threshold; $i++) {
            $seqId = $this->getSeqId($seqName);
            $lastInsertId = $seqId + 1;

            $res = $this->setSeqId($seqName, $lastInsertId);
            if ($res) {
                return $lastInsertId;
                break;
            }
        }

        return false;
    }

    /**
     * 通过redis获取全局主键值,利用redis全局互斥锁
     * @param string $seqName 分表对应逻辑表名称,不带分表名称的后缀
     */
    private function _getPKbyRedis($seqName)
    {
        $redisObj = RedisService::connectRedis();
        if (!is_object($redisObj)) {
            throw new \Exception("failed to create redis object!");
        }
        $lockKey = "sequence_{$seqName}_lock";
        $seqKey = "sequence_{$seqName}";


        //检测全局互斥锁是否能设置进去
        while(true) {
            $lockRes = $redisObj->setnx($lockKey,time());
            if ($lockRes) { //如果可以锁定,就直接退出循环,进入下一步流程
                break;
            }

            //如果互斥锁是锁定状态,那么判断时间是否过期
            $time = $redisObj->get($lockKey);
            if (time() - $time > 1) {   //如果循环等待时间大于1秒,则不再等待
                break;
            }
        }

        //将redis中全局主键值+1
        $lastInsertId = $redisObj->incr($seqKey);

        //如果增量操作失败,
        if (intval($lastInsertId) === 1 || $lastInsertId === false) {
            $lastInsertId = $this->getSeqId($seqName);
            if(!is_numeric($lastInsertId)) {
                throw new \Exception('failed to get gid from db');
            }

            $res = $redisObj->set($seqKey,$lastInsertId);
            if ($res === false) {
                throw new \Exception("failed to set gid [$seqKey]");
            }

            $lastInsertId = $redisObj->incr($seqKey);
            if (!$lastInsertId) {
                throw new \Exception("failed to incr gid [$seqKey]");
            }
        }

        $this->setSeqId($seqName,$lastInsertId);
        $redisObj->del($lockKey);

        return $lastInsertId;
    }

    /**
     * @param $seqName
     * @return mixed
     */
    private function getSeqId($seqName)
    {
        $sql = "SELECT gid FROM gc_sequence where seq_name='{$seqName}'";
        $data = $this->mysqlObj->query($sql)->fetch(\PDO::FETCH_ASSOC);
        return $data['gid'];
    }

    /**
     * @param $seqName
     * @param $lastInsertId
     * @return mixed
     */
    private function setSeqId($seqName, $lastInsertId)
    {
        $sql = "UPDATE gc_sequence SET gid={$lastInsertId} WHERE seq_name='{$seqName}' AND gid < $lastInsertId";
        $res = $this->mysqlObj->exec($sql);
        return $res;
    }
}